package com.yscp.ysdemo.filter;

import com.auth0.jwt.interfaces.DecodedJWT;

import com.yscp.ysdemo.constant.JWTConstants;
import com.yscp.ysdemo.constant.RedisConstant;
import com.yscp.ysdemo.constant.ResultEnum;
import com.yscp.ysdemo.dto.LoginUser;
import com.yscp.ysdemo.exception.BusinessException;
import com.yscp.ysdemo.utils.JWTUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//OncePerRequestFilter由spring提供，特点是只执行一次，如果直接继承普通的filter不是很好
//OncePerRequestFilter顾名思义，他能够确保在一次请求只通过一次filter，
//        而不需要重复执行 大家常识上都认为，一次请求本来就只过一次,还要OncePerRequestFilter限制一次过滤干啥。
//        其实实际上因为不同的web container对请求的过滤方式都不一样，比如在servlet-2.3中，Filter会过滤一切请求，
//        包括服务器内部使用forward转发请求和<%@ include file="/index.jsp"%>的情况。到了servlet-2.4中Filter
//        默认下只拦截外部提交的请求，forward和include这些内部转发都不会被过滤，但是有时候我们需要 forward的时候也
//        用到Filter。



//首先整个流程对用户的认证是需要token有效+redis存储有用户数据，
//redis存数据主要是为了复用，少查数据库，其二是让用户登录”有状态“，通过判断redis中是否有对应内容做第二重判断，
//日后如果要踢用户下线或者需要感知用户是否为登录状态就可以通过redis来判断，到这点为止不用userid用其他当key也行，
//但如果牵扯到更改数据库用户数据，而redis需要及时更新保持一致性时，用userid做key就可以很快在redis定位并修改，但
//如果使用uuid或者token当key，还需要在这之前额外存储它们，很麻烦。其二，使用userid还能保证redis只存一份用户数据，
//比如当高并发下可能存在重复登录，如果用其他key可能就导致redis存储重复数据浪费空间，但userid的唯一性确保了数据的唯一性。

//感觉这套流程下来使用useId作为key存入redis也是有一定道理的
//
/**
 * 检查请求是否携带token，如果携带了需要从redis得到user信息存入SecurityContext
 * 该方法不是UsernamePasswordAuthenticationFilter的替代品，实际上是分离开了
 *
 * 这里已经额外进行“记住我”的默认实现，jwt这里设置7天过期时间，当jwt过期时顺便把redis的用户删除即可，而且在
 * redis中的用户数据也应该设置过期时间，以防用户长期不登录触发不了jwt过期删redis的逻辑
 *
 *
 *
 * 但如果要实现长久免登陆，即jwt刷新续签就相对麻烦，我想到的方法有2种，
 * 1.要么设置redis信息不过期，通过redis信息是否存在，来决定是否需要刷新token，如果token过期就生成新的返回前端;
 *
 * 2或者通过jwt本身的有效时间判断，不过这样不稳定，如果jwt
 * 和redis都设置有效时间，如果用户长时间不登录到2者都过期，那么还是要重新登陆，如果设置redis信息不过期那就不如用第一种
 *
 * 前面2种都需要前端写额外的逻辑去判断每个请求是否返回了新的token，编码成本也不低
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate redisTemplate;

    //该过滤器放在UsernamePasswordAuthenticationFilter之前
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //这里"token"是和前端商量好让前端在请求头插入该命名的token键值对
        String token = request.getHeader("token");
//        如果是用户登录等不需要登录的请求，此时还没有token，放行就行了
//        （这种请求整条认证链都需要放行的，毕竟系统肯定也有不需要登录也可以访问的东西,比如登录请求），而登录用户是必须携带token来访问的
        if(StringUtils.isBlank(token)){
            filterChain.doFilter(request,response);
            return;
        }
//        得到token内的用户id信息
        DecodedJWT tokenInfo = JWTUtils.getTokenInfo(token);
        String userId = tokenInfo.getClaim(JWTConstants.USER_ID).asString();
        String redisKey = RedisConstant.getLoginToken(userId);
//        从redis中根据id取出对应用户信息
        LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(redisKey);
//        这里再判断一次redis中是否有用户信息，因为jwt的无状态，如果管理人强制用户下线，会清除redis对应用户信息
//        所以这里还得判断实际上用户的状态
        if (loginUser == null){
             throw new BusinessException(ResultEnum.NEED_LOGIN);
        }
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
//        存入该容器,类似于ThreadLocal，后续安全框架会从该容器取出权限信息进行后续认证
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(request,response);

    }
}
